Skip to content

Fix MBOMsg wire layout (order_id/price/ts_recv offsets) and StatMsg undefined-quantity sentinel - #46

Merged
tbeason merged 1 commit into
mainfrom
fix/mbo-wire-layout
Sep 10, 2026
Merged

tbeason merged 1 commit into
mainfrom
fix/mbo-wire-layout

Conversation

@tbeason

@tbeason tbeason commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Summary

Two wire-format bugs, found while generating DuckDB fixtures for duckdb-dbn with this package's encoder (the DuckDB reader, which uses the databento-cpp struct layout, disagreed with us on both — and it was right).

1. MBOMsg field offsets were swapped (decode and encode)

read_mbo_msg read ts_recv from bytes 16–23, order_id from 24–31 and price from 40–47. The official MboMsg (dbn crate; identical in DBN v1/v2/v3) is order_id @16, price @24, size @32, flags @36, channel_id @37, action @38, side @39, ts_recv @40, ts_in_delta @48, sequence @52 — i.e. struct order == wire order.

Because write_record(::MBOMsg) mirrored the decoder, Julia round trips were byte-stable and every self-consistency test passed. But any MBO file produced by Databento decoded with the three fields rotated. On Databento's own test_data.mbo.dbn:

field this package (before) official decoder / raw bytes
order_id 3722750000000 (the price) 647784973705
price 1609160400000704060 (the ts_recv) 3722750000000 = 3722.75
ts_recv 647784973705 (the order_id) 1609160400000704060

test/test_phase5.jl had pinned the swapped values (r1.order_id == 3722750000000); corrected. New test/test_mbo_wire_layout.jl decodes every test_data.mbo.* fixture against reference values, checks a byte-exact re-encode of the uncompressed fixtures, and decodes a hand-laid-out record.

Impact: any consumer of MBO price / order_id / ts_recv through this package or DatabentoAPI.jl's Live client got rotated values. Files captured by decode→re-encode (DatabentoAPI stream_to_file) are byte-identical to the wire and decode correctly after this fix. Files written from Julia-built records (write_dbn, DBNStreamWriter, csv_to_dbn / json_to_dbn / parquet_to_dbn) carry the swapped layout on disk — see the CHANGELOG migration note.

2. StatMsg undefined quantity written as -1

The encoder wrote an undefined v3 quantity (typemax(Int64)) as 0xffffffffffffffff (= -1) instead of the spec sentinel typemax(Int64) (0x7fff…). The v3 decoder read a UInt64 and mapped every value >= 0x7fff… — i.e. any negative quantity — to UNDEF, which masked the bug in round trips; other readers showed -1 where they should show NULL/NaN. Quantity is now written and read as a signed Int64. New test/test_stat_quantity_sentinel.jl.

Tests

julia +release --project=. -e 'using Pkg; Pkg.test()'
DBN.jl Tests  | 3906   3906  55.8s

Independent of #45 (both branch from main) and no overlap in src/. Both PRs do add an include(...) line at the same spot in test/runtests.jl and an ## [Unreleased] section at the top of CHANGELOG.md, so whichever merges second needs a trivial conflict resolution (keep both). Line endings preserved (git diff --stat == --ignore-cr-at-eol).

🤖 Generated with Claude Code

https://claude.ai/code/session_01YBr6EZa1vHrJmPBoecSjRa

read_mbo_msg / write_record(::MBOMsg) used a field order that does not match
the DBN spec (ts_recv at 16, order_id at 24, price at 40). The official MboMsg
(identical in v1/v2/v3) has order_id at 16, price at 24, ts_recv at 40. The
encoder mirrored the decoder, so Julia round trips and all self-consistency
tests passed, but every Databento-produced MBO file decoded with the three
fields rotated. Both paths now use the official layout; test_phase5.jl's
fixture assertions (which had pinned the swapped values) are corrected and
test_mbo_wire_layout.jl decodes Databento's own fixtures against reference
values plus a byte-exact re-encode.

The StatMsg encoder wrote an undefined v3 quantity as 0xffffffffffffffff (-1)
instead of typemax(Int64); the v3 decoder read a UInt64 and mapped every value
>= 0x7fff... (any negative) to UNDEF, which masked it. Quantity is now written
and read as a signed Int64 (test_stat_quantity_sentinel.jl).

Found while generating DuckDB test fixtures for duckdb-dbn, whose reader
(databento-cpp layout) disagreed with this package on both points.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBr6EZa1vHrJmPBoecSjRa
@tbeason
tbeason merged commit 37cd25a into main Sep 10, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant